package com.zzyl.nursing.service.impl;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
import com.zzyl.common.constant.CacheConstants;
import com.zzyl.nursing.domain.AlertData;
import com.zzyl.nursing.domain.DeviceData;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.service.IAlertDataService;
import com.zzyl.nursing.vo.AlertNotifyVo;
import com.zzyl.nursing.websocket.WebSocketServer;
import com.zzyl.system.mapper.SysUserRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import com.zzyl.nursing.mapper.AlertRuleMapper;
import com.zzyl.nursing.domain.AlertRule;
import com.zzyl.nursing.service.IAlertRuleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

/**
 * 报警规则Service业务层处理
 *
 * @author ruoyi
 * @date 2025-09-21
 */
@Slf4j
@Service
public class AlertRuleServiceImpl extends ServiceImpl<AlertRuleMapper, AlertRule> implements IAlertRuleService {
    @Autowired
    private AlertRuleMapper alertRuleMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private DeviceMapper deviceMapper;
    @Autowired
    private SysUserRoleMapper sysUserRoleMapper;

    @Value("${alert.deviceMaintainerRole}")
    private String deviceMaintainerRoleName;

    @Value("${alert.managerRole}")
    private String managerRoleName;
    @Autowired
    private IAlertDataService alertDataService;
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 查询报警规则
     *
     * @param id 报警规则主键
     * @return 报警规则
     */
    @Override
    public AlertRule selectAlertRuleById(Long id) {
        return getById(id);
    }

    /**
     * 查询报警规则列表
     *
     * @param alertRule 报警规则
     * @return 报警规则
     */
    @Override
    public List<AlertRule> selectAlertRuleList(AlertRule alertRule) {
        return alertRuleMapper.selectAlertRuleList(alertRule);
    }

    /**
     * 新增报警规则
     *
     * @param alertRule 报警规则
     * @return 结果
     */
    @Override
    public int insertAlertRule(AlertRule alertRule) {
        return save(alertRule) ? 1 : 0;
    }

    /**
     * 修改报警规则
     *
     * @param alertRule 报警规则
     * @return 结果
     */
    @Override
    public int updateAlertRule(AlertRule alertRule) {
        return updateById(alertRule) ? 1 : 0;
    }

    /**
     * 批量删除报警规则
     *
     * @param ids 需要删除的报警规则主键
     * @return 结果
     */
    @Override
    public int deleteAlertRuleByIds(Long[] ids) {
        return removeByIds(Arrays.asList(ids)) ? 1 : 0;
    }

    /**
     * 删除报警规则信息
     *
     * @param id 报警规则主键
     * @return 结果
     */
    @Override
    public int deleteAlertRuleById(Long id) {
        return removeById(id) ? 1 : 0;
    }

    /**
     * 1、报警过滤（每分钟执行一次）
     */
    @Override
    public void alertFilter() {
        // 1. 查询启用中的报警规则条数（第一步判断是否存在报警规则）
        Long count = this.lambdaQuery().eq(AlertRule::getStatus, 1).count();
        // 2. 判断查询到的规则条数是否为空，如果为空，直接结束定时任务
        if (count == 0) {
            return;
        }
        // 3. 如果不为空，则从Redis中获取所有设备的最新一次上报数据的大集合（大集合中包含多个类型为List<DeviceData>的小集合）
        List<Object> deviceLastDataList = redisTemplate.opsForHash().values(CacheConstants.IOT_DEVICE_LAST_DATA);
        // 4. 如果获取的所有设备最新一次上报数据为空，直接结束定时任务
        if (ObjectUtil.isEmpty(deviceLastDataList)) {
            return;
        }
        // 5. 如果不为空，对所有设备的上报数据进行数据格式标准化处理，合并两个集合为一个集合，将大集合中的小集合平铺开，去除掉嵌套结构，存入到一个新的待检查数据集合中
        // 合并后，待检查集合waitCheckDeviceDataList中存储的一个个DeviceData，就是所有设备的所有属性
        List<DeviceData> waitCheckDeviceDataList = new ArrayList<>();
        deviceLastDataList.forEach(item -> waitCheckDeviceDataList.addAll((List<DeviceData>) item));
        // 6. 遍历待检查数据集合，依次对每一个属性进行检测，判断是否达到报警阈值
//        waitCheckDeviceDataList.forEach(deviceData -> alertCheck(deviceData));
        waitCheckDeviceDataList.forEach(this::alertCheck);
    }

    /**
     * 2、报警检测
     *
     * @param deviceData 某个设备的上报数据中的某个属性
     */
    private void alertCheck(DeviceData deviceData) {
        // 1. 如果该属性的最新一次上报数据已经超过一分钟了，则没必要再继续检测（因为大概率已经检测过了，设备已经停了，没有再上报新的数据了）
        LocalDateTime alarmTime = deviceData.getAlarmTime();
        long betweenSeconds = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS);
        if (betweenSeconds > 60) {
            return;
        }
        // 2. 查询该设备所属产品的通用规则（IotId = -1）
        List<AlertRule> commonRules = this.lambdaQuery().eq(AlertRule::getProductKey, deviceData.getProductKey())
                .eq(AlertRule::getIotId, -1)
                .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                .eq(AlertRule::getStatus, 1)
                .list();
        // 3. 查询该设备的特有规则
        List<AlertRule> deviceRules = this.lambdaQuery().eq(AlertRule::getProductKey, deviceData.getProductKey())
                .eq(AlertRule::getIotId, deviceData.getIotId())
                .eq(AlertRule::getFunctionId, deviceData.getFunctionId())
                .eq(AlertRule::getStatus, 1)
                .list();
        // 4. 合并所有适用于该设备的规则
        Collection<AlertRule> alertRules = CollUtil.addAll(commonRules, deviceRules);
        // 5. 判断合并后的规则，如果为空，直接结束方法，返回for循环中，检测下一个属性的数据
        if (ObjectUtil.isEmpty(alertRules)) {
            return;
        }
        // 6. 不为空，则遍历报警规则，与该属性的数据进行比对
        alertRules.forEach(rule -> deviceDataRuleCheck(rule, deviceData));
    }

    /**
     * 3、使用报警规则对上报数据进行检测
     *
     * @param rule       规则
     * @param deviceData 数据（属性）
     */
    private void deviceDataRuleCheck(AlertRule rule, DeviceData deviceData) {
        // 1. 判断该条数据是否在 该条规则的时间范围内
        // 1.1 获取该数据的上报时间 和 规则内的开始时间、结束时间(00:00:00~23:59:59)
        String[] split = rule.getAlertEffectivePeriod().split("~");
        LocalTime startTime = LocalTime.parse(split[0]);
        LocalTime endTime = LocalTime.parse(split[1]);
        LocalTime alarmTime = deviceData.getAlarmTime().toLocalTime();
        // 1.2 比较数据上报时间是否在规则的生效时间范围，如果不在，直接结束，或者进入下一条规则的比对
        if (alarmTime.isBefore(startTime) || alarmTime.isAfter(endTime)) {
            return;
        }
        // 2. 判断该条数据是否达到了该规则的报警阈值（上报数据 + 操作符 + 阈值 如：60 <= 65)
        // 2.1 组装报警数（达到报警阈值的次数）的Redis Key
        String alertCountKey = CacheConstants.IOT_COUNT_ALERT + deviceData.getIotId() + deviceData.getFunctionId() + rule.getId();
        // 2.2 获取该条数据值与报警阈值的比较结果，-1为小于，0为等于，1为大于
        int compareResult = Double.valueOf(deviceData.getDataValue()).compareTo(rule.getValue());
        if ((">=".equals(rule.getOperator()) && compareResult >= 0) || ("<".equals(rule.getOperator()) && compareResult < 0)) {
            // 2.3 如果达到了阈值，可以继续往下走
            log.info("当前上报数据: {}，达到了报警阈值：{}", deviceData, rule);
        } else { // 2.4 如果没有达到阈值，则删除Redis中的报警数，结束，或者进入下一条规则的比对
            redisTemplate.delete(alertCountKey);
            return;
        }
        // 3. 如果达到了报警规则阈值，再判断是否在沉默周期内
        // 3.1 组装沉默周期key
        String silentKey = CacheConstants.IOT_SILENT_ALERT + deviceData.getIotId() + deviceData.getFunctionId() + rule.getId();
        // 3.2 判断沉默周期key是否存在，如果存在，说明当前还在沉默周期内，不触发报警，结束这条规则的检测流程
        if (redisTemplate.hasKey(silentKey)) {
            return;
        }
        // 4. 如果不在沉默周期内，报警数+1，然后继续判断报警数是否达到了持续周期，如果等于，才可以最终报警
        // 4.1 报警数+1
        Long count = redisTemplate.opsForValue().increment(alertCountKey);
        // 4.2 判断报警数是否等于持续周期，若不等于，结束这条规则的检测流程，如果等于，继续往下走
        if (count < rule.getDuration()) {
            return;
        }
        // 5. 此时报警数已经达到了持续周期，且不在沉默周期内，删除历史的报警数，报警
        redisTemplate.delete(alertCountKey);
        // 6. 既然马上触发报警，添加沉默周期，防止继续累加报警数，并为Redis缓存中添加沉默周期key，设置key的生效时间为沉默周期的时间
        redisTemplate.opsForValue().set(silentKey, 1, Duration.ofMinutes(rule.getAlertSilentPeriod()));
        // 7. 根据异常数据的类型，找到需要报警通知的人员的id，开始保存报警数据（从这里开始走报警的流程）
        List<Long> userIds = new ArrayList<>();
        // 8.如果异常数据类型等于0，则说明是老人异常数据，再判断是老人的随身设备还是固定设备，再通知护理人员
        if (rule.getAlertDataType() == 0) {
            // 8.1 判断老人的设备类型，如果是随时设备，通过设备找到老人对应的护理人员
            if (deviceData.getLocationType() == 0){
                userIds = deviceMapper.selectNursingIdByIotIdWithElder(deviceData.getIotId());
            } else if (deviceData.getLocationType() == 1 && deviceData.getPhysicalLocationType() == 2) {
                // 8.2 如果是老人固定设备，则通过床位找到老人对应的护理人员
                userIds = deviceMapper.selectNursingIdByIotIdWithBed(deviceData.getIotId());
            }
        }else { // 否则为固定设备异常数据，直接通知维修工即可
            userIds = sysUserRoleMapper.selectByRoleName(deviceMaintainerRoleName);
        }
        // 9. 找到所有的超级管理员
        List<Long> adminUserId = sysUserRoleMapper.selectByRoleName(managerRoleName);
        // 10. 合并需要被通知的人员id，并去重，避免有人员一人兼多角，重复通知
        List<Long> needNotifyUserIds = CollUtil.distinct(CollUtil.addAll(userIds, adminUserId));
        // 11. 保存报警数据到alert_data表
        AlertData alertData = batchSaveAlertData(rule, deviceData, needNotifyUserIds);
        // 12. 调用WebSocket推送报警消息，弹框提醒
        webSocketNotify(rule, alertData, needNotifyUserIds);
    }

    /**
     * 调用WebSocket推送报警消息，弹框提醒
     * @param rule
     * @param alertData
     * @param needNotifyUserIds
     */
    private void webSocketNotify(AlertRule rule, AlertData alertData, List<Long> needNotifyUserIds) {
        AlertNotifyVo alertNotifyVo = BeanUtil.toBean(alertData, AlertNotifyVo.class);
        alertNotifyVo.setFunctionName(rule.getFunctionName());
        alertNotifyVo.setAlertDataType(rule.getAlertDataType());
        alertNotifyVo.setNotifyType(1);

        alertNotifyVo.setIsAllConsumer(false);
        webSocketServer.sendMessageToConsumer(alertNotifyVo, needNotifyUserIds);
    }

    /**
     * 批量保存报警数据到alert_data表
     *
     * @param rule
     * @param deviceData
     * @param needNotifyUserIds 待通知的人员id
     */
    private AlertData batchSaveAlertData(AlertRule rule, DeviceData deviceData, List<Long> needNotifyUserIds) {
        log.error("开始通知养老院工作人员：{}", needNotifyUserIds);
        // 1. 从deviceData中拷贝数据到AlertData中，需要忽略主键，手动将拷贝过来的id值赋为null
        AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class);
        alertData.setId(null);
        // 2. 填充AlertData，补全属性
        alertData.setAlertRuleId(rule.getId()); // 填充报警规则id
        // 2.1 填充报警原因
//        String alertReason = rule.getFunctionId() + rule.getOperator() + rule.getValue() + rule.getDuration() + rule.getAlertSilentPeriod();
        StringBuffer sb = new StringBuffer();
        sb.append(rule.getFunctionId()).append(rule.getOperator()).append(rule.getValue())
                        .append("持续：").append(rule.getDuration()).append("个周期，沉默周期：")
                        .append(rule.getAlertSilentPeriod()).append("分钟");
        alertData.setAlertReason(sb.toString());
        // 2.2 填充报警数据类型
        alertData.setType(rule.getAlertDataType());
        // 2.3 填充报警状态，默认为待处理0
        alertData.setStatus(0);
        // 3. 遍历待通知的人员id，组装AlertData集合
        List<AlertData> dbList = needNotifyUserIds.stream()
                .map(userId -> BeanUtil.toBean(alertData, AlertData.class).setUserId(userId))
                .collect(Collectors.toList());
        // 4. 批量保存
        alertDataService.saveBatch(dbList);
        return alertData;
    }

}
